Skip to content

test: Shared test infrastructure — MockHttpMessageHandler, fixtures & helpers (#51)#53

Merged
teesofttech merged 5 commits into
masterfrom
feature/issue-51-test-infrastructure
Jun 12, 2026
Merged

test: Shared test infrastructure — MockHttpMessageHandler, fixtures & helpers (#51)#53
teesofttech merged 5 commits into
masterfrom
feature/issue-51-test-infrastructure

Conversation

@teesofttech

@teesofttech teesofttech commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #51

Implements all shared test infrastructure that every per-gateway test file will depend on.

What's included

Helpers/MockHttpMessageHandler.cs

  • Queue-based HttpMessageHandler — no live network calls
  • Supports multi-step gateways (e.g. Monnify OAuth then init, Interswitch OAuth then pay) via sequential .RespondWith() chaining
  • Assertion helpers: AssertRequestCount, AssertLastRequestPath, AssertLastMethod
  • BuildClient(baseAddress) convenience method

Helpers/PaymentRequestFactory.cs

  • PaymentRequestFactory.Build(configure?) — valid PaymentRequest with all fields set
  • PaymentRequestFactory.BuildRefund(configure?) — valid RefundRequest
  • GatewayConfigFactory.Build{GatewayName}(...) — one builder per gateway (all 15)

Helpers/IntegrationTestBase.cs

  • Abstract base for integration test classes
  • Reads required env vars at construction; exposes SkipIfMissingEnvVars()
  • CI output clearly shows which env vars are missing

paybridge.runsettings

  • dotnet test --filter Category=Unit → unit tests only
  • dotnet test --filter Category=Integration → integration tests only
  • Code coverage scoped to PayBridge.SDK.dll

Unit/TestInfrastructureTests.cs

  • 20 unit tests exercising all helpers

Test results

Test summary: total: 20, failed: 0, succeeded: 20, skipped: 0

Summary by CodeRabbit

  • New Features

    • Added PeachPayments gateway: create payments, verify transactions, and process refunds; gateway selectable by currency and transaction reference; configuration support added.
  • Tests

    • New test helpers, mocks, and factories plus comprehensive unit and integration tests; test runsettings and test project configuration included to support coverage and selective runs.
  • Chores

    • CI workflows added for unit and integration test execution and artifact collection.

- Add PeachPayments = 15 to PaymentGatewayType enum
- Add PeachPaymentsConfig (EntityId, AccessToken, IsSandbox)
- Implement PeachPaymentsGateway: checkout initiation, payment verification, refund
- Register gateway in IServiceCollectionExtensions and PaymentGatewayFactory
- Add ZAR/KES/NGN/BWP routing to PeachPayments in PaymentService
- Add PEACH_ prefix detection for gateway resolution
… helpers (#51)

- Add MockHttpMessageHandler: queue-based fake HTTP handler with
  assertion helpers (AssertRequestCount, AssertLastRequestPath,
  AssertLastMethod). Supports multi-step gateways via sequential queuing.

- Add PaymentRequestFactory: builds valid PaymentRequest and
  RefundRequest with sensible defaults; supports configure lambda.

- Add GatewayConfigFactory: one builder per gateway, sets only the
  minimum keys required to pass constructor validation.

- Add IntegrationTestBase: abstract base that checks env vars and
  exposes SkipIfMissingEnvVars() for clean CI output.

- Add paybridge.runsettings: supports dotnet test --filter Category=Unit
  and --filter Category=Integration with code coverage config.

- Add TestInfrastructureTests: 20 unit tests covering all helpers.

Closes #51
Copilot AI review requested due to automatic review settings June 11, 2026 15:25
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared test utilities (mock HTTP handler, payment/gateway factories, integration test base, runsettings), unit tests for those utilities, CI workflows for unit and integration tests, and a full PeachPayments gateway implementation wired into DI and selection logic.

Changes

Test Infrastructure and PeachPayments Gateway

Layer / File(s) Summary
Mock HTTP Handler and Request Builders
PayBridge.SDK.Test/Helpers/MockHttpMessageHandler.cs, PayBridge.SDK.Test/Helpers/PaymentRequestFactory.cs
MockHttpMessageHandler queues responses and records requests with fluent APIs and assertion helpers. PaymentRequestFactory and GatewayConfigFactory provide test object builders with defaults and per-gateway configuration.
Integration Test Base and Project Configuration
PayBridge.SDK.Test/Helpers/IntegrationTestBase.cs, PayBridge.SDK.Test/PayBridge.SDK.Test.csproj, PayBridge.SDK.Test/paybridge.runsettings
IntegrationTestBase validates required environment variables and exposes skip helpers. Test project adds test dependencies and a project reference; runsettings configures test categories and code coverage collection.
Test Infrastructure Validation
PayBridge.SDK.Test/Unit/TestInfrastructureTests.cs
Unit tests validate MockHttpMessageHandler response queuing, multi-call sequences, request recording, assertions, and factory-generated objects across multiple gateways including Paystack, Monnify, Squad, Interswitch, and PeachPayments.
PeachPayments Configuration and Enum
PayBridge.SDK/Dtos/PaymentGatewayConfig.cs, PayBridge.SDK/Enums/PaymentGatewayType.cs
PaymentGatewayConfig adds PeachPaymentsConfig property with EntityId, AccessToken, and IsSandbox fields. PaymentGatewayType extends with PeachPayments enum member (value 15).
PeachPayments Gateway Implementation
PayBridge.SDK/Gateways/PeachPaymentsGateway.cs
CreatePaymentAsync POSTs checkout parameters to /checkouts and returns widget URL. VerifyPaymentAsync GETs from /payments and parses transaction status and details. RefundPaymentAsync POSTs refund data to /payments/{transactionReference}. All use bearer auth, log request/response bodies, and throw PaymentGatewayException on error.
PeachPayments Service Wiring and Routing
PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs, PayBridge.SDK/Factories/PaymentGatewayFactory.cs, PayBridge.SDK/Services/PaymentService.cs
Service registration adds PeachPaymentsGateway to DI container for both default and explicit gateway selection. PaymentService currency-based selection includes PeachPayments for multi-currency and adds BWP-specific routing. DetermineGatewayFromReference maps "PEACH_" transaction prefix to PeachPayments.
CI Workflows
.github/workflows/ci-tests.yml, .github/workflows/integration-tests.yml
Adds unit test workflow and scheduled/manual integration workflow; integration workflow injects gateway secrets from GitHub Secrets and uploads TRX artifacts.

Sequence Diagram(s):

sequenceDiagram
  participant Test as TestCode
  participant Gateway as PeachPaymentsGateway
  participant API as PeachPaymentsAPI
  Test->>Gateway: CreatePaymentAsync(request)
  Gateway->>API: POST /checkouts (Bearer auth)
  API-->>Gateway: JSON response (result.code, id, description)
  Gateway-->>Test: PaymentResponse (status, widget URL)
Loading

Estimated code review effort:
🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #45 — MockHttpMessageHandler and env-var skipping used for Interswitch tests; this PR provides those test infra pieces and gateway config builders.

"🐰 I built mocks and queues with care,
Responses ready, requests laid bare,
PeachPayments hums with POST and GET,
Tests skip gentle when secrets are yet,
The bridge hops on — all checks prepared!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning All changes are directly aligned with #51 requirements except for PeachPayments gateway additions (PaymentGatewayConfig, PaymentGatewayType, IServiceCollectionExtensions, PaymentGatewayFactory, PeachPaymentsGateway, PaymentService) and CI workflows, which appear to be scope creep beyond the test infrastructure focus. Remove or defer PeachPayments gateway implementation files and CI workflow changes to separate PR focused on adding the new payment gateway, keeping this PR scoped to test infrastructure only.
Docstring Coverage ⚠️ Warning Docstring coverage is 26.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main changes: adding shared test infrastructure including MockHttpMessageHandler, test fixtures, and helper classes for both unit and integration tests.
Linked Issues check ✅ Passed All coding requirements from issue #51 are met: MockHttpMessageHandler with queue-based responses and assertions, PaymentRequestFactory/GatewayConfigFactory for all 15 gateways, IntegrationTestBase with env var validation, test categorization via traits, runsettings for filtering, and proper directory structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-51-test-infrastructure

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
PayBridge.SDK.Test/Unit/TestInfrastructureTests.cs (1)

77-130: ⚡ Quick win

Add missing coverage for AssertLastMethod and IntegrationTestBase skip behavior.

The suite validates most helper paths but skips two core behaviors from this infrastructure set: method assertion helper and env-var-driven skip logic. Adding these targeted tests would close the remaining acceptance-risk gaps.

Also applies to: 134-224

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK.Test/Unit/TestInfrastructureTests.cs` around lines 77 - 130,
Add two tests: one covering MockHttpMessageHandler.AssertLastMethod and one
covering IntegrationTestBase's env-var-driven skip logic. For AssertLastMethod,
mirror existing request-path tests: create a MockHttpMessageHandler,
RespondWith(...), perform a request with HttpMethod.Post (and another with
HttpMethod.Get), then call handler.AssertLastMethod(HttpMethod.Post) to assert
it does not throw and a mismatched call to AssertLastMethod expects
InvalidOperationException with a message containing the expected vs actual
method. For IntegrationTestBase, add tests that set and unset the environment
variable that IntegrationTestBase reads (e.g., RUN_INTEGRATION_TESTS or the
actual key used in IntegrationTestBase) and assert the class/method that
determines skipping
(IntegrationTestBase.Skip/ShouldSkip/ShouldRunIntegrationTests) returns the
expected boolean for both enabled and disabled values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PayBridge.SDK.Test/Helpers/IntegrationTestBase.cs`:
- Around line 36-48: Modify the IntegrationTestBase constructor to load the
.env.test file before evaluating required env vars: call your dotenv loader
(e.g., DotNetEnv.Load(".env.test") or equivalent safe loader) at the start of
the constructor (before computing missing) so Environment.GetEnvironmentVariable
reads values from .env.test; keep the existing _requiredVars assignment and then
compute missing and set SkipReason as before, and ensure the loader is tolerant
if .env.test is absent (no exception).

In `@PayBridge.SDK.Test/Helpers/MockHttpMessageHandler.cs`:
- Around line 24-31: The mock currently dequeues the next MockHttpResponse for
any request; update MockHttpMessageHandler and MockHttpResponse so each queued
response includes expected HttpMethod and URL pattern and make the request
handler (SendAsync) validate the incoming HttpRequestMessage against those
expectations: if the head response matches method and URL pattern then dequeue
and return it, otherwise throw an exception signaling an unexpected request
(include actual request method/URL and expected values). Also update the
RespondWith overloads (the methods that enqueue MockHttpResponse) to
accept/record expected method and URL pattern and ensure unit tests using
RespondWith supply those values.

In `@PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs`:
- Around line 142-143: The registration registers only the concrete
PeachPaymentsGateway, so it won't be discovered when PaymentService resolves
IEnumerable<IPaymentGateway>; change the DI registration(s) from
services.AddScoped<PeachPaymentsGateway>() to
services.AddScoped<IPaymentGateway, PeachPaymentsGateway>() (and apply the same
fix to the other occurrence around the second registration block), ensuring
IPaymentGateway is the service type so PaymentService receives
PeachPaymentsGateway via IEnumerable<IPaymentGateway>.

In `@PayBridge.SDK/Gateways/PeachPaymentsGateway.cs`:
- Around line 195-197: The refund payload in PeachPaymentsGateway is hardcoding
["currency"] = "ZAR"; update the refund construction to use the actual
transaction currency (e.g., request.Currency or the original payment's currency)
instead of "ZAR" so refunds honor multi-currency flows; locate the refund
payload assembly in PeachPaymentsGateway (where request.Amount.ToString("F2",
...) is used) and replace the literal "ZAR" with the appropriate currency
property from the request/transaction object and add a null/empty fallback if
needed.
- Around line 143-145: The parsing assumes JsonDocument.Parse(body).RootElement
is an array and directly indexes json[0], which throws on an empty array; update
the parsing in PeachPaymentsGateway (where json and payment are set) to first
check if json.ValueKind == JsonValueKind.Array and json.GetArrayLength() == 0
and in that case treat it as a failed verification (return the normal failed
verification response) instead of accessing json[0]; otherwise proceed to set
payment = json.ValueKind == JsonValueKind.Array ? json[0] : json and continue.
- Around line 206-223: The refund handler currently parses 'body' and inspects
JSON properties (resultEl, codeEl, idEl) without validating the HTTP response
status; update the refund method in PeachPaymentsGateway (the code that builds
the RefundResponse using json, resultEl, resultCode, refundId) to first check
the HTTP response's IsSuccessStatusCode (or StatusCode) and treat non-success
responses as failures: avoid parsing/assuming JSON for error responses, set
Success=false, Status=PaymentStatus.Failed, include a meaningful Message using
the HTTP status and any safe body text, and only parse result/code/description
into RefundResponse when the HTTP status is successful. Ensure
RefundResponse.RefundReference/TransactionReference/Amount remain populated as
appropriate when marking a failure.

In `@PayBridge.SDK/Services/PaymentService.cs`:
- Around line 262-269: The currency routing is incorrectly including
PaymentGatewayType.PeachPayments for XOF, XAF and MWK even though PeachPayments
only supports ZAR/KES/NGN/BWP/USD; update the switch branch that returns
ChooseAvailableGateway for those currencies to remove
PaymentGatewayType.PeachPayments so the call becomes
ChooseAvailableGateway(PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup,
PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack); ensure only
gateways declared to support those currencies remain in that
ChooseAvailableGateway invocation.

---

Nitpick comments:
In `@PayBridge.SDK.Test/Unit/TestInfrastructureTests.cs`:
- Around line 77-130: Add two tests: one covering
MockHttpMessageHandler.AssertLastMethod and one covering IntegrationTestBase's
env-var-driven skip logic. For AssertLastMethod, mirror existing request-path
tests: create a MockHttpMessageHandler, RespondWith(...), perform a request with
HttpMethod.Post (and another with HttpMethod.Get), then call
handler.AssertLastMethod(HttpMethod.Post) to assert it does not throw and a
mismatched call to AssertLastMethod expects InvalidOperationException with a
message containing the expected vs actual method. For IntegrationTestBase, add
tests that set and unset the environment variable that IntegrationTestBase reads
(e.g., RUN_INTEGRATION_TESTS or the actual key used in IntegrationTestBase) and
assert the class/method that determines skipping
(IntegrationTestBase.Skip/ShouldSkip/ShouldRunIntegrationTests) returns the
expected boolean for both enabled and disabled values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5426765f-8870-4ee5-8e7f-e80a5eaff852

📥 Commits

Reviewing files that changed from the base of the PR and between 2667022 and 272c623.

📒 Files selected for processing (12)
  • PayBridge.SDK.Test/Helpers/IntegrationTestBase.cs
  • PayBridge.SDK.Test/Helpers/MockHttpMessageHandler.cs
  • PayBridge.SDK.Test/Helpers/PaymentRequestFactory.cs
  • PayBridge.SDK.Test/PayBridge.SDK.Test.csproj
  • PayBridge.SDK.Test/Unit/TestInfrastructureTests.cs
  • PayBridge.SDK.Test/paybridge.runsettings
  • PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
  • PayBridge.SDK/Enums/PaymentGatewayType.cs
  • PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
  • PayBridge.SDK/Factories/PaymentGatewayFactory.cs
  • PayBridge.SDK/Gateways/PeachPaymentsGateway.cs
  • PayBridge.SDK/Services/PaymentService.cs

Comment on lines +36 to +48
protected IntegrationTestBase(params string[] requiredEnvVars)
{
_requiredVars = requiredEnvVars;

var missing = requiredEnvVars
.Where(v => string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(v)))
.ToList();

if (missing.Count > 0)
{
SkipReason = $"Integration test skipped — missing env var(s): {string.Join(", ", missing)}";
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

.env.test source is not loaded by this base class.

This implementation only reads process environment variables; it does not load values from .env.test, which is part of the stated integration-test objective. Please add explicit .env.test loading (or equivalent bootstrap) before missing-var evaluation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK.Test/Helpers/IntegrationTestBase.cs` around lines 36 - 48,
Modify the IntegrationTestBase constructor to load the .env.test file before
evaluating required env vars: call your dotenv loader (e.g.,
DotNetEnv.Load(".env.test") or equivalent safe loader) at the start of the
constructor (before computing missing) so Environment.GetEnvironmentVariable
reads values from .env.test; keep the existing _requiredVars assignment and then
compute missing and set SkipReason as before, and ensure the loader is tolerant
if .env.test is absent (no exception).

Comment on lines +24 to +31
public MockHttpMessageHandler RespondWith(
HttpStatusCode statusCode,
string jsonBody,
string contentType = "application/json")
{
_queue.Enqueue(new MockHttpResponse(statusCode, jsonBody, contentType));
return this;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Mock queue does not enforce expected HTTP method/URL per response.

The handler currently dequeues the next response for any incoming request, so a wrong endpoint/method can still pass if the queue is non-empty. That misses the acceptance criterion for queued (method, URL pattern, statusCode, responseBody) matching and “unexpected request” failures.

Also applies to: 33-41, 49-63

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK.Test/Helpers/MockHttpMessageHandler.cs` around lines 24 - 31,
The mock currently dequeues the next MockHttpResponse for any request; update
MockHttpMessageHandler and MockHttpResponse so each queued response includes
expected HttpMethod and URL pattern and make the request handler (SendAsync)
validate the incoming HttpRequestMessage against those expectations: if the head
response matches method and URL pattern then dequeue and return it, otherwise
throw an exception signaling an unexpected request (include actual request
method/URL and expected values). Also update the RespondWith overloads (the
methods that enqueue MockHttpResponse) to accept/record expected method and URL
pattern and ensure unit tests using RespondWith supply those values.

Comment on lines +142 to 143
services.AddScoped<PeachPaymentsGateway>();
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register PeachPayments as IPaymentGateway, not only concrete type.

PaymentService builds _gateways from IEnumerable<IPaymentGateway>. Current registration adds only PeachPaymentsGateway, so PeachPayments can be missing from runtime gateway selection.

Suggested fix
- services.AddScoped<PeachPaymentsGateway>();
+ AddGatewayRegistration<PeachPaymentsGateway>(services);
...
- case PaymentGatewayType.PeachPayments:
-     services.AddScoped<PeachPaymentsGateway>();
-     break;
+ case PaymentGatewayType.PeachPayments:
+     AddGatewayRegistration<PeachPaymentsGateway>(services);
+     break;

Also applies to: 194-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs` around lines 142 -
143, The registration registers only the concrete PeachPaymentsGateway, so it
won't be discovered when PaymentService resolves IEnumerable<IPaymentGateway>;
change the DI registration(s) from services.AddScoped<PeachPaymentsGateway>() to
services.AddScoped<IPaymentGateway, PeachPaymentsGateway>() (and apply the same
fix to the other occurrence around the second registration block), ensuring
IPaymentGateway is the service type so PaymentService receives
PeachPaymentsGateway via IEnumerable<IPaymentGateway>.

Comment on lines +143 to +145
var json = JsonDocument.Parse(body).RootElement;
var payment = json.ValueKind == JsonValueKind.Array ? json[0] : json;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard empty array responses in verification parsing.

When /payments returns an empty array, json[0] throws and the method fails via exception instead of returning a normal failed verification response.

Suggested fix
- var json = JsonDocument.Parse(body).RootElement;
- var payment = json.ValueKind == JsonValueKind.Array ? json[0] : json;
+ var json = JsonDocument.Parse(body).RootElement;
+ JsonElement payment;
+ if (json.ValueKind == JsonValueKind.Array)
+ {
+     if (json.GetArrayLength() == 0)
+     {
+         return new VerificationResponse
+         {
+             Success = false,
+             TransactionReference = transactionReference,
+             Status = PaymentStatus.Failed,
+             Message = "PeachPayments verification returned no payment records"
+         };
+     }
+     payment = json[0];
+ }
+ else
+ {
+     payment = json;
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var json = JsonDocument.Parse(body).RootElement;
var payment = json.ValueKind == JsonValueKind.Array ? json[0] : json;
var json = JsonDocument.Parse(body).RootElement;
JsonElement payment;
if (json.ValueKind == JsonValueKind.Array)
{
if (json.GetArrayLength() == 0)
{
return new VerificationResponse
{
Success = false,
TransactionReference = transactionReference,
Status = PaymentStatus.Failed,
Message = "PeachPayments verification returned no payment records"
};
}
payment = json[0];
}
else
{
payment = json;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Gateways/PeachPaymentsGateway.cs` around lines 143 - 145, The
parsing assumes JsonDocument.Parse(body).RootElement is an array and directly
indexes json[0], which throws on an empty array; update the parsing in
PeachPaymentsGateway (where json and payment are set) to first check if
json.ValueKind == JsonValueKind.Array and json.GetArrayLength() == 0 and in that
case treat it as a failed verification (return the normal failed verification
response) instead of accessing json[0]; otherwise proceed to set payment =
json.ValueKind == JsonValueKind.Array ? json[0] : json and continue.

Comment on lines +195 to +197
["amount"] = request.Amount.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
["currency"] = "ZAR",
["paymentType"] = "RF",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Refund currency is hardcoded to ZAR in a multi-currency gateway flow.

Create payment supports multiple currencies, but refund payload always sends ZAR. Non-ZAR transactions can fail or be refunded against the wrong currency context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Gateways/PeachPaymentsGateway.cs` around lines 195 - 197, The
refund payload in PeachPaymentsGateway is hardcoding ["currency"] = "ZAR";
update the refund construction to use the actual transaction currency (e.g.,
request.Currency or the original payment's currency) instead of "ZAR" so refunds
honor multi-currency flows; locate the refund payload assembly in
PeachPaymentsGateway (where request.Amount.ToString("F2", ...) is used) and
replace the literal "ZAR" with the appropriate currency property from the
request/transaction object and add a null/empty fallback if needed.

Comment on lines +206 to +223
var json = JsonDocument.Parse(body).RootElement;
var resultCode = json.TryGetProperty("result", out var resultEl) &&
resultEl.TryGetProperty("code", out var codeEl)
? codeEl.GetString() : null;

bool success = resultCode != null && resultCode.StartsWith("000");
var refundId = json.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? string.Empty : string.Empty;

return new RefundResponse
{
Success = success,
RefundReference = refundId,
TransactionReference = request.TransactionReference,
Amount = request.Amount,
Status = success ? PaymentStatus.Refunded : PaymentStatus.Failed,
Message = resultEl.TryGetProperty("description", out var descEl)
? descEl.GetString() ?? "Unknown" : "Unknown"
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refund flow should validate HTTP status before trusting payload.

The method currently derives refund success from JSON content without checking IsSuccessStatusCode, which can misclassify failed HTTP calls and is fragile for non-JSON error bodies.

Suggested fix
 var response = await _httpClient.PostAsync($"{BaseUrl}/payments/{request.TransactionReference}", content);
 var body = await response.Content.ReadAsStringAsync();
 _logger.LogDebug("PeachPayments refund response: {Body}", body);

+if (!response.IsSuccessStatusCode)
+{
+    return new RefundResponse
+    {
+        Success = false,
+        TransactionReference = request.TransactionReference,
+        Amount = request.Amount,
+        Status = PaymentStatus.Failed,
+        Message = $"PeachPayments refund failed: {response.StatusCode}"
+    };
+}
+
 var json = JsonDocument.Parse(body).RootElement;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Gateways/PeachPaymentsGateway.cs` around lines 206 - 223, The
refund handler currently parses 'body' and inspects JSON properties (resultEl,
codeEl, idEl) without validating the HTTP response status; update the refund
method in PeachPaymentsGateway (the code that builds the RefundResponse using
json, resultEl, resultCode, refundId) to first check the HTTP response's
IsSuccessStatusCode (or StatusCode) and treat non-success responses as failures:
avoid parsing/assuming JSON for error responses, set Success=false,
Status=PaymentStatus.Failed, include a meaningful Message using the HTTP status
and any safe body text, and only parse result/code/description into
RefundResponse when the HTTP status is successful. Ensure
RefundResponse.RefundReference/TransactionReference/Amount remain populated as
appropriate when marking a failure.

Comment on lines 262 to 269
case "XOF":
case "XAF":
case "MWK":
return ChooseAvailableGateway(PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);
return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);

case "BWP":
return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.DpoGroup);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Currency routing includes PeachPayments for currencies outside its declared support.

This selection path now prioritizes PeachPayments for XOF, XAF, and MWK, while the gateway itself documents support for ZAR/KES/NGN/BWP/USD. That mismatch increases avoidable payment failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Services/PaymentService.cs` around lines 262 - 269, The
currency routing is incorrectly including PaymentGatewayType.PeachPayments for
XOF, XAF and MWK even though PeachPayments only supports ZAR/KES/NGN/BWP/USD;
update the switch branch that returns ChooseAvailableGateway for those
currencies to remove PaymentGatewayType.PeachPayments so the call becomes
ChooseAvailableGateway(PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup,
PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack); ensure only
gateways declared to support those currencies remain in that
ChooseAvailableGateway invocation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces shared test infrastructure for the PayBridge.SDK.Test project, but it also adds a new production payment gateway (Peach Payments) and updates runtime gateway-selection logic in PayBridge.SDK.

Changes:

  • Adds shared test helpers: MockHttpMessageHandler, request/config factories, and IntegrationTestBase, plus unit tests and a .runsettings file for filtering/coverage.
  • Introduces PeachPaymentsGateway and wires it into DI/factory/config + updates gateway selection and reference-prefix detection.
  • Updates test project dependencies to include xUnit, FluentAssertions, Moq, and coverage tooling.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
PayBridge.SDK/Services/PaymentService.cs Adds PeachPayments to currency-based gateway routing and reference-prefix detection.
PayBridge.SDK/Gateways/PeachPaymentsGateway.cs New Peach Payments gateway implementation (create/verify/refund).
PayBridge.SDK/Factories/PaymentGatewayFactory.cs Includes PeachPayments in gateway instantiation.
PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs Registers PeachPaymentsGateway in DI.
PayBridge.SDK/Enums/PaymentGatewayType.cs Adds PeachPayments enum value.
PayBridge.SDK/Dtos/PaymentGatewayConfig.cs Adds PeachPaymentsConfig to gateway configuration.
PayBridge.SDK.Test/Unit/TestInfrastructureTests.cs Adds unit tests for the new test infrastructure (partial coverage).
PayBridge.SDK.Test/PayBridge.SDK.Test.csproj Adds test dependencies and project reference to SDK.
PayBridge.SDK.Test/paybridge.runsettings Adds test filtering guidance and code coverage scoping.
PayBridge.SDK.Test/Helpers/PaymentRequestFactory.cs Adds PaymentRequestFactory and GatewayConfigFactory builders for tests.
PayBridge.SDK.Test/Helpers/MockHttpMessageHandler.cs Adds queued-response HTTP handler for tests.
PayBridge.SDK.Test/Helpers/IntegrationTestBase.cs Adds base class for env-var-gated integration tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +265 to +268
return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);

case "BWP":
return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.DpoGroup);
Comment on lines 262 to 269
case "XOF":
case "XAF":
case "MWK":
return ChooseAvailableGateway(PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);
return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);

case "BWP":
return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.DpoGroup);

Comment on lines +143 to +145
var json = JsonDocument.Parse(body).RootElement;
var payment = json.ValueKind == JsonValueKind.Array ? json[0] : json;

Comment on lines +49 to +66
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
_requests.Add(request);

if (_queue.Count == 0)
{
throw new InvalidOperationException(
$"MockHttpMessageHandler: no queued response for {request.Method} {request.RequestUri}. " +
"Did you forget to call RespondWith()?");
}

var mock = _queue.Dequeue();
var response = new HttpResponseMessage(mock.StatusCode)
{
Content = new StringContent(mock.Body, Encoding.UTF8, mock.ContentType)
};
Comment on lines +21 to +38
private readonly string[] _requiredVars;

/// <summary>
/// The reason string reported by xUnit when a test is skipped.
/// Set in the constructor if any env var is missing.
/// </summary>
protected string? SkipReason { get; }

/// <summary>
/// Whether all required env vars are present.
/// Use this in test bodies to conditionally Skip:
/// <code>Skip.If(ShouldSkip, SkipReason);</code>
/// </summary>
protected bool ShouldSkip => SkipReason != null;

protected IntegrationTestBase(params string[] requiredEnvVars)
{
_requiredVars = requiredEnvVars;
Comment on lines +36 to +48
protected IntegrationTestBase(params string[] requiredEnvVars)
{
_requiredVars = requiredEnvVars;

var missing = requiredEnvVars
.Where(v => string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(v)))
.ToList();

if (missing.Count > 0)
{
SkipReason = $"Integration test skipped — missing env var(s): {string.Join(", ", missing)}";
}
}
Comment on lines +202 to +206
var response = await _httpClient.PostAsync($"{BaseUrl}/payments/{request.TransactionReference}", content);
var body = await response.Content.ReadAsStringAsync();
_logger.LogDebug("PeachPayments refund response: {Body}", body);

var json = JsonDocument.Parse(body).RootElement;
- ci-tests.yml: runs on every push/PR to master and feature/* branches
  - Filters to Category=Unit only (no network, no secrets needed)
  - Publishes TRX results via dorny/test-reporter as PR comment
  - Uploads coverage.cobertura.xml artifact

- integration-tests.yml: manual trigger (workflow_dispatch) + nightly cron
  - Runs Category=Integration tests with all 15 gateway env vars injected
  - Uses a protected 'integration' GitHub environment for secret scoping
  - fail-on-error=false so missing sandbox keys skip rather than fail

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci-tests.yml:
- Line 20: Add the checkout step option to disable credential persistence:
update the actions/checkout step that uses "actions/checkout@v4" to include
"with: persist-credentials: false" so the checkout token is not exposed to
downstream steps; ensure you apply the same change to the other workflow(s) that
call actions/checkout@v4 to keep behavior consistent.
- Line 20: The workflow uses mutable action tags (e.g., actions/checkout@v4)
which should be replaced with pinned commit SHAs to remove supply-chain risk;
locate every "uses:" entry referencing actions/checkout@v4,
actions/setup-dotnet@v4, actions/upload-artifact@v4, dorny/test-reporter@v1, and
softprops/action-gh-release@v2 and update each to the corresponding
repository@<commit-sha> (fetch the latest stable commit SHA from the action's
GitHub repo), commit the updated uses: lines, and run/validate the workflow to
ensure syntax and behavior remain correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eceb1f09-4ee2-4a3d-93c8-6625c1db54c7

📥 Commits

Reviewing files that changed from the base of the PR and between 272c623 and 18c0c52.

📒 Files selected for processing (2)
  • .github/workflows/ci-tests.yml
  • .github/workflows/integration-tests.yml


steps:
- name: Checkout repository
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shared root cause: checkout token persistence not disabled.
Both workflows should set with: persist-credentials: false on actions/checkout to reduce credential exposure in downstream steps.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 19-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 20-20: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-tests.yml at line 20, Add the checkout step option to
disable credential persistence: update the actions/checkout step that uses
"actions/checkout@v4" to include "with: persist-credentials: false" so the
checkout token is not exposed to downstream steps; ensure you apply the same
change to the other workflow(s) that call actions/checkout@v4 to keep behavior
consistent.

Source: Linters/SAST tools


⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List workflow files
echo "== Workflows =="
ls -la .github/workflows || true
echo

# Show lines around the referenced area in ci-tests.yml
echo "== ci-tests.yml (around lines 1-80) =="
nl -ba .github/workflows/ci-tests.yml | sed -n '1,120p'
echo

# Find all workflow files and grep for unpinned action uses (tag-only, major-only, etc.)
echo "== Unpinned action references (uses: owner/repo@v*) =="
rg -n "uses:\s*[^/@]+/[^/@]+@v[^#\s]+" .github/workflows || true
echo

echo "== Unpinned action references (uses: owner/repo@<major>.* without sha) =="
rg -n "uses:\s*[^/@]+/[^/@]+@v[0-9]+(?![0-9a-f]{7,40})" .github/workflows || true
echo

echo "== Pinned action references (uses with sha) =="
rg -n "uses:\s*[^/@]+/[^/@]+@[0-9a-f]{7,40}" .github/workflows || true
echo

# If there are multiple workflows, dump their uses blocks for quick inspection
echo "== Dump uses: lines per workflow =="
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
  [ -e "$f" ] || continue
  echo "--- $f ---"
  rg -n "uses:\s*" "$f" || true
done

Repository: teesofttech/PayBridge

Length of output: 495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Workflows =="
ls -la .github/workflows || true
echo

echo "== ci-tests.yml (show full file with line numbers) =="
cat -n .github/workflows/ci-tests.yml | sed -n '1,200p'
echo

echo "== integration-tests.yml (show full file with line numbers) =="
cat -n .github/workflows/integration-tests.yml | sed -n '1,260p'
echo

echo "== nuget-publish.yml (show full file with line numbers) =="
cat -n .github/workflows/nuget-publish.yml | sed -n '1,260p'
echo

echo "== Unpinned action references (uses: owner/repo@v* without sha) across workflows =="
# Matches uses: owner/repo@v<digits/letters> (common tags like v4, v3.1, v2) excluding sha-like 7-40 hex
rg -n --hidden --no-ignore-vcs "uses:\s*[^/@]+/[^/@]+@v[0-9][^#\s]*" .github/workflows || true

echo "== Uses references that look like they include a full/partial commit sha =="
rg -n --hidden --no-ignore-vcs "uses:\s*[^/@]+/[^/@]+@[0-9a-f]{7,40}" .github/workflows || true

echo "== All uses: lines across workflows =="
rg -n --hidden --no-ignore-vcs "^\s*uses:\s*" .github/workflows || true

Repository: teesofttech/PayBridge

Length of output: 11419


Pin GitHub Actions to immutable commit SHAs (avoid tag-based uses:).
CI/integration/release workflows use mutable major tags (no SHA), e.g. actions/checkout@v4, actions/setup-dotnet@v4, actions/upload-artifact@v4, dorny/test-reporter@v1, and softprops/action-gh-release@v2 (see .github/workflows/ci-tests.yml, .github/workflows/integration-tests.yml, .github/workflows/nuget-publish.yml). This creates a supply-chain risk if a tag is moved or a compromised action version is published.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 19-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 20-20: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-tests.yml at line 20, The workflow uses mutable action
tags (e.g., actions/checkout@v4) which should be replaced with pinned commit
SHAs to remove supply-chain risk; locate every "uses:" entry referencing
actions/checkout@v4, actions/setup-dotnet@v4, actions/upload-artifact@v4,
dorny/test-reporter@v1, and softprops/action-gh-release@v2 and update each to
the corresponding repository@<commit-sha> (fetch the latest stable commit SHA
from the action's GitHub repo), commit the updated uses: lines, and run/validate
the workflow to ensure syntax and behavior remain correct.

Source: Linters/SAST tools

- Remove double-dash from XML comments in paybridge.runsettings
  (invalid XML: 'An XML comment cannot contain --')
- Remove --settings flag from both workflows (runsettings only
  needed for local coverage; was causing the runner to reject it)
- Rename workflow title to 'CI - Unit Tests' (dash safe in YAML)
- Add fail-on-empty: false to dorny/test-reporter so it does not
  error when the TRX file is absent due to an upstream failure
GitHub denies test-reporter from creating check runs on PRs unless
the workflow explicitly grants 'checks: write' and 'pull-requests: write'.
Added permissions block to both ci-tests.yml and integration-tests.yml.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/ci-tests.yml (1)

14-16: ⚡ Quick win

Add a job timeout to prevent runaway CI jobs.

The job has no timeout-minutes set. Adding a timeout prevents runaway jobs from consuming CI resources indefinitely and provides faster feedback when something hangs.

⏱️ Suggested addition
 unit-tests:
   name: Unit Tests (.NET 8)
   runs-on: ubuntu-latest
+  timeout-minutes: 15

(Adjust the timeout value based on typical execution time; 15 minutes is a reasonable starting point for unit tests.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-tests.yml around lines 14 - 16, The CI job "unit-tests"
currently has no timeout and can hang indefinitely; add a timeout by setting the
GitHub Actions job field timeout-minutes under the unit-tests job (e.g.,
timeout-minutes: 15 or another appropriate value) to limit how long the
unit-tests job can run and prevent runaway CI usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci-tests.yml:
- Around line 14-16: The CI job "unit-tests" currently has no timeout and can
hang indefinitely; add a timeout by setting the GitHub Actions job field
timeout-minutes under the unit-tests job (e.g., timeout-minutes: 15 or another
appropriate value) to limit how long the unit-tests job can run and prevent
runaway CI usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 64860cd1-ee53-4d99-90a4-5c60fc36d6ad

📥 Commits

Reviewing files that changed from the base of the PR and between c8f21d2 and c52bd19.

📒 Files selected for processing (2)
  • .github/workflows/ci-tests.yml
  • .github/workflows/integration-tests.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/integration-tests.yml

@teesofttech
teesofttech merged commit 23e69c9 into master Jun 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: Shared test infrastructure — MockHttpMessageHandler, fixtures & helpers

2 participants